import Dataset from '../dataset'
import Geom from '../geom'
import Commands from '../commands'
import Common from '../common.js'
import utils from '../utils'
import paths from '../paths'
import {Bounds} from '../utils'
import ShpConverterText from '../shpconverter-text'
import {GeoJSON} from './index.js' 
import Geojson from './index.js'

function exportGeoJSON(dataset, opts) {
  opts = opts || {}
  let extension = opts.extension || "json"
  let layerGroups = null

  // Apply coordinate precision, if relevant
  if (opts.precision || opts.rfc7946) {
    dataset = Dataset.copyDatasetForExport(dataset)
    // using 6 decimals as default RFC 7946 precision
    Geom.setCoordinatePrecision(dataset, opts.precision || 0.000001)
  }

  // if (opts.rfc7946) {
  //   warn = getRFC7946Warnings(dataset)
  //   if (warn) message(warn)
  // }

  if (opts.file) {
    // Override default output extension if output filename is given
    extension = utils.getFileExtension(opts.file)
  }
  if (opts.combine_layers) {
    layerGroups = [dataset.layers]
  } else {
    layerGroups = dataset.layers.map(lyr => {
      return [lyr]
    })
  }
  return layerGroups.map(layers => {
    // Use common part of layer names if multiple layers are being merged
    const name = Commands.mergeLayerNames(layers) || 'output'
    const d = utils.defaults({layers: layers}, dataset)
    return {
      content: exportDatasetAsGeoJSON(d, opts, 'buffer'),
      filename: name + '.' + extension
    }
  })
}
const exporters = {
  polygon: exportPolygonGeom,
  polyline: exportLineGeom,
  point: exportPointGeom
}
// Return an array of Features or Geometries as objects or strings
function exportLayerAsGeoJSON(lyr, dataset, opts, asFeatures, ofmt) {
  const properties = exportProperties(lyr.data, opts)
  const shapes = lyr.shapes
  const ids = exportIds(lyr.data, opts)
  let stringify = null

  if (ofmt) {
    stringify = opts.prettify ?
      Geojson.getFormattedStringify(['bbox', 'coordinates']) :
      JSON.stringify
  }

  if (properties && shapes && properties.length !== shapes.length) {
    utils.error("Mismatch between number of properties and number of shapes")
  }

  return (shapes || properties || []).reduce((memo, o, i) => {
    const shape = shapes ? shapes[i] : null
    const exporter = exporters[lyr.geometry_type]
    let obj = shape ? exporter(shape, dataset.arcs, opts) : null
    if (asFeatures) {
      obj = {
        type: 'Feature',
        geometry: obj,
        properties: properties ? properties[i] : null
      }
      if (ids) {
        obj.id = ids[i]
      }
    } else if (!obj) {
      return memo // don't add null objects to GeometryCollection
    }
    if (ofmt) {
      // stringify features as soon as they are generated, to reduce the
      // number of JS objects in memory (so larger files can be exported)
      obj = stringify(obj)
      if (ofmt == 'buffer') {
        obj = ShpConverterText.encodeString(obj, 'utf8')
        // obj = internal.stringToBuffer(obj);
        // obj = new Buffer(obj, 'utf8');
      }
    }
    memo.push(obj)
    return memo
  }, [])
}
function getRFC7946Warnings(dataset) {
  const P = Geom.getDatasetCRS(dataset)
  let str = ''
  if (!P || !Geom.isLatLngCRS(P)) {
    str = 'RFC 7946 warning: non-WGS84 coordinates.'
    if (P) str += ' Use "-proj wgs84" to convert.'
  }
  return str
}
function getDatasetBbox(dataset, rfc7946) {
  const P = Geom.getDatasetCRS(dataset)
  const wrapped = rfc7946 && P && Geom.isLatLngCRS(P)
  const westBounds = new Bounds()
  const eastBounds = new Bounds()

  dataset.layers.forEach(lyr => {
    if (Common.layerHasPaths(lyr)) {
      utils.traversePaths(lyr.shapes, null, function(o) {
        const bounds = dataset.arcs.getSimpleShapeBounds(o.arcs)
        (bounds.centerX() < 0 ? westBounds : eastBounds).mergeBounds(bounds)
      })
    } else if (Common.layerHasPoints(lyr)) {
      utils.forEachPoint(lyr.shapes, function(p) {
        (p[0] < 0 ? westBounds : eastBounds).mergePoint(p[0], p[1])
      })
    }
  })
  const mergedBounds = (new Bounds()).mergeBounds(eastBounds).mergeBounds(westBounds)
  if (mergedBounds.hasBounds()) {
    const bbox = mergedBounds.toArray()
  }
  if (wrapped && eastBounds.hasBounds() && westBounds.hasBounds()) {
    const gutter = eastBounds.xmin - westBounds.xmax
    const margins = 360 + westBounds.xmin - eastBounds.xmax
    if (gutter > 0 && gutter > margins) {
      bbox[0] = eastBounds.xmin
      bbox[2] = westBounds.xmax
    }
  }
  return bbox || null
}
function exportDatasetAsGeoJSON(dataset, opts, ofmt) {
  const geojson = {}
  const layers = dataset.layers
  const useFeatures = useFeatureCollection(layers, opts)
  let collname = null

  if (useFeatures) {
    geojson.type = 'FeatureCollection'
    collname = 'features'
  } else {
    geojson.type = 'GeometryCollection'
    collname = 'geometries'
  }

  if (!opts.rfc7946) {
    // partial support for crs property (eliminated in RFC 7946)
    exportCRS(dataset, geojson)
  }

  if (opts.bbox) {
    const bbox = getDatasetBbox(dataset, opts.rfc7946)
    if (bbox) {
      geojson.bbox = bbox
    }
  }

  const collection = layers.reduce(function(memo, lyr, i) {
    const items = exportLayerAsGeoJSON(lyr, dataset, opts, useFeatures, ofmt)
    return memo.length > 0 ? memo.concat(items) : items
  }, [])

  if (opts.geojson_type == 'Feature' && collection.length == 1) {
    return collection[0]
  } else if (ofmt) {
    return formatGeoJSON(geojson, collection, collname, ofmt)
  } else {
    geojson[collname] = collection
    return geojson
  }
}
function formatGeoJSON(container, collection, collType, ofmt) {
  // collection is an array of individual GeoJSON Feature|geometry strings or buffers
  const head = JSON.stringify(container).replace(/\}$/, ', "' + collType + '": [\n')
  const tail = '\n]}'
  if (ofmt == 'buffer') {
    return joinOutputBuffers(head, tail, collection)
  }
  return head + collection.join(',\n') + tail
}
function joinOutputBuffers(head, tail, collection) {
  const comma = utils.createBuffer(',\n', 'utf8')
  const parts = collection.reduce(function(memo, buf, i) {
    if (i > 0) memo.push(comma)
    memo.push(buf)
    return memo
  }, [utils.createBuffer(head, 'utf8')])
  parts.push(utils.createBuffer(tail, 'utf8'))
  return Buffer.concat(parts)
}
// export GeoJSON or TopoJSON point geometry
function exportPointGeom(points, arcs) {
  let geom = null
  if (points.length == 1) {
    geom = {
      type: "Point",
      coordinates: points[0]
    }
  } else if (points.length > 1) {
    geom = {
      type: "MultiPoint",
      coordinates: points
    }
  }
  return geom
}

function exportLineGeom(ids, arcs) {
  const obj = paths.exportPathData(ids, arcs, "polyline")
  if (obj.pointCount === 0) return null
  const coords = obj.pathData.map(function(path) {
    return path.points
  })
  return coords.length == 1 ? {
    type: "LineString",
    coordinates: coords[0]
  } : {
    type: "MultiLineString",
    coordinates: coords
  }
}

function exportPolygonGeom(ids, arcs, opts) {
  const obj = paths.exportPathData(ids, arcs, "polygon")
  if (obj.pointCount === 0) return null
  const groups = utils.groupPolygonRings(obj.pathData, opts.invert_y)
  const reverse = opts.rfc7946 && !opts.invert_y
  const coords = groups.map(function(paths) {
    return paths.map(function(path) {
      if (reverse) path.points.reverse()
      return path.points
    })
  })
  return coords.length == 1 ? {
    type: "Polygon",
    coordinates: coords[0]
  } : {
    type: "MultiPolygon",
    coordinates: coords
  }
}
// @jsonObj is a top-level GeoJSON or TopoJSON object
// TODO: generate crs if projection is known
// TODO: handle case of non-WGS84 geodetic coordinates
function exportCRS(dataset, jsonObj) {
  const info = dataset.info || {}
  if (!info.crs && 'input_geojson_crs' in info) {
    // use input geojson crs if available and coords have not changed
    jsonObj.crs = info.input_geojson_crs
  } else if (info.crs && !Geom.isLatLngCRS(info.crs)) {
    // Setting output crs to null if coords have been projected
    // "If the value of CRS is null, no CRS can be assumed"
    // source: http://geojson.org/geojson-spec.html#coordinate-reference-system-objects
    jsonObj.crs = null
  } else {
    // crs property not set: assuming WGS84
  }
}
function useFeatureCollection(layers, opts) {
  const type = opts.geojson_type || ''
  if (type == 'Feature' || type == 'FeatureCollection') {
    return true
  } else if (type == 'GeometryCollection') {
    return false
  } else if (type) {
    // stop("Unsupported GeoJSON type:", opts.geojson_type)
    utils.error(`Unsupported GeoJSON type:${opts.geojson_type}`)
  }
  // default is true iff layers contain attributes
  return utils.some(layers, lyr => {
    const fields = lyr.data ? lyr.data.getFields() : []
    const haveData = useFeatureProperties(fields, opts)
    const haveId = !!getIdField(fields, opts)
    return haveData || haveId
  })
}
function useFeatureProperties(fields, opts) {
  return !(opts.drop_table || opts.cut_table || fields.length === 0 ||
      fields.length == 1 && fields[0] == GeoJSON.ID_FIELD)
}
function exportProperties(table, opts) {
  const fields = table ? table.getFields() : []
  const idField = getIdField(fields, opts)
  let properties = null
  if (!useFeatureProperties(fields, opts)) {
    return null
  }
  const records = table.getRecords()
  if (idField == GeoJSON.ID_FIELD) {// delete default id field, not user-set fields
    properties = records.map(rec => {
      rec = utils.extend({}, rec) // copy rec;
      delete rec[idField]
      return rec
    })
  } else {
    properties = records
  }
  return properties
}
// @opt value of id-field option (empty, string or array of strings)
// @fields array
function getIdField(fields, opts) {
  let ids = []
  const opt = opts.id_field
  if (utils.isString(opt)) {
    ids.push(opt)
  } else if (utils.isArray(opt)) {
    ids = opt
  }
  ids.push(GeoJSON.ID_FIELD) // default id field
  return utils.find(ids, name => {
    return utils.contains(fields, name)
  })
}
function exportIds(table, opts) {
  const fields = table ? table.getFields() : []
  const idField = getIdField(fields, opts)
  if (!idField) return null
  return table.getRecords().map(rec => {
    return idField in rec ? rec[idField] : null
  })
}
export default {
  exportGeoJSON,
}